package it.unicam.cs.cosy.bioshape.gui.visualizer3d;

import hermesV3.util.HermesLogger;
import it.unicam.cs.cosy.bioshape.gui.Monitor;

import javax.imageio.ImageIO;
import javax.media.j3d.AmbientLight;
import javax.media.j3d.Appearance;
import javax.media.j3d.Background;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.DirectionalLight;
import javax.media.j3d.Font3D;
import javax.media.j3d.FontExtrusion;
import javax.media.j3d.GeometryArray;
import javax.media.j3d.GraphicsConfigTemplate3D;
import javax.media.j3d.GraphicsContext3D;
import javax.media.j3d.Group;
import javax.media.j3d.ImageComponent;
import javax.media.j3d.ImageComponent2D;
import javax.media.j3d.LineArray;
import javax.media.j3d.LineAttributes;
import javax.media.j3d.Node;
import javax.media.j3d.OrientedShape3D;
import javax.media.j3d.PolygonAttributes;
import javax.media.j3d.Raster;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Text3D;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.View;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileNameExtensionFilter;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GraphicsConfigTemplate;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Map;

import com.sun.j3d.utils.universe.*;
import javax.vecmath.*;

import com.sun.j3d.utils.behaviors.vp.*;

/**
 * GUI Panel for showing the simulation stored in the .simu file
 * 
 * @author Federico Buti
 */
public class VisualizationPanel extends JPanel implements ActionListener {
	/** Auto-generated serial ID */
	private static final long serialVersionUID = 1L;
	private JPanel controls;
	private static final int PWIDTH = 950; // size of panel
	private static final int PHEIGHT = 712;

	private final int BOUNDSIZE; // larger than world

	// initial user position
	private final Point3d USERPOSN;

	private Vector3d tempVector = new Vector3d();
	private JFileChooser fc;
	private static final int UPDATE_TIME = 100; // ms, for updating the balls

	private SimpleUniverse simpleUniverse;
	private BranchGroup sceneBranchGroup;
	private BoundingSphere bounds; // for environment nodes
	private Canvas3D canvas3D;
	private Transform3D t3d = new Transform3D();
	private UpdateBehaviour timer;
	private JSlider slider;
	private OrbitBehavior orbit;
	private TransformGroup steerTG;
	private String fps;
	private JSlider slider2;
	private JButton play;
	private Transform3D ty = new Transform3D();
	private double previousValue;
	private Point3d min;
	private Point3d max;
	final private static int SPLITX = 1;
	final private static int SPLITY = 2;
	final private static int SPLITZ = 3;
	int splitAxis;
	private double offset = -1;
	private int locals = -1;
	private Point3d center;
	private ImageIcon playIcon;
	private ImageIcon pauseIcon;

	/**
	 * Builds the visualisation context, from the given input file. The file has
	 * extension ".geom" and has been generated by the XML file passed as input
	 * to the simulator.
	 * 
	 * @param file
	 *            {@code String} path to the file containing the set up and
	 *            geometry information.
	 * @throws IOException
	 *             If any I/O error occurs.
	 */
	public VisualizationPanel(String file) throws IOException {
		this(new File(file));
	}

	/**
	 * Builds the visualisation context, from the given input file. The file has
	 * extension ".geom" and has been generated by the XML file passed as input
	 * to the simulator.
	 * 
	 * @param geomFile
	 *            {@code File} containing the set up and geometry information.
	 * @throws IOException
	 *             If any I/O error occurs.
	 */
	public VisualizationPanel(File geomFile) throws IOException {
		if (geomFile.isDirectory() || !geomFile.getName().contains(".geom"))
			throw new IllegalArgumentException("The passed reference is not "
					+ "a file or it is not a correct geometry file.");
		//
		setLayout(new BorderLayout());
		// choose the best graphics setting
		GraphicsDevice graphicsDevice[] = GraphicsEnvironment.getLocalGraphicsEnvironment()
				.getScreenDevices();
		// extract simulation points from the input file
		extractPoints(geomFile);
		center = new Point3d((max.x + min.x) / 2d, (max.y + min.y) / 2d,
				(max.z + min.z) / 2d);
		USERPOSN = new Point3d(center.x, center.y, min.distance(max) * 2);
		// calculate bounds
		Point3d origin = new Point3d(0, 0, 0);
		double distMin = Math.abs(min.distance(origin));
		double distMax = Math.abs(max.distance(origin));
		// TODO Luca: che è?
		BOUNDSIZE = (int) Math.round(distMin < distMax ? distMax : distMin) * 100;
		//
		createCanvasWithExtension(graphicsDevice);
		// add canvas to the panel
		add(canvas3D, BorderLayout.CENTER);
		canvas3D.setFocusable(true);
		canvas3D.requestFocus();
		// build support structures for interaction
		simpleUniverse = new SimpleUniverse(canvas3D);
		Viewer viewer = simpleUniverse.getViewer();
		View view = viewer.getView();
		view.setFrontClipDistance(1);
		view.setBackClipDistance(2000); // (back / front) < 3000, MANDATORY!
		// create scene, coordinates and set the user position (point of view)
		createSceneGraph();
		drawSimulatedWorld(); // the simulated world bounds
		drawSimulatedWorldCoordinateFrameAtOrigin(); // the coordinates object at the origin
		putLightsOnTheScene(); // the lights
		setSceneBackgroundColor(); // the sky
		setUpBehaviours(geomFile);
		drawSimulatedWorldCoordinateFrameAtOrigin(); // Luca: perché di nuovo?
		initUserPosition(USERPOSN);
		// FIX THE SCENE (at least what can be fixed...)
		sceneBranchGroup.compile();
		simpleUniverse.addBranchGraph(sceneBranchGroup);
		HermesLogger.log(HermesLogger.INFO, getClass().getSimpleName() + " - "
				+ "generating visualiser...");
		orbitControls();
		createControls();
		// setOpaque(false);
		setPreferredSize(new Dimension(PWIDTH, PHEIGHT));
	}

	/**
	 * Generates the {@code Canvas3D}, i.e. the main object in which the scene
	 * is rendered. It is extended to provide the text overlay with hardware
	 * information and FPS counter.
	 * 
	 * @param gd
	 *            Object wrapping the visualisation devide.
	 */
	private void createCanvasWithExtension(GraphicsDevice[] gd) {
		GraphicsConfigTemplate3D gc3D = new GraphicsConfigTemplate3D();
		gc3D.setSceneAntialiasing(GraphicsConfigTemplate.PREFERRED);
		//
		canvas3D = new Canvas3D(gd[0].getBestConfiguration(gc3D)) {
			private static final long serialVersionUID = 7144426579917281131L;
			ArrayList<String> info = reportTextureUnitInfo();

			private ArrayList<String> reportTextureUnitInfo() {
				ArrayList<String> data = new ArrayList<String>();
				Map<?, ?> c3dMap = this.queryProperties();
				if (c3dMap.containsKey("textureUnitStateMax")) {
					int max = ((Integer) c3dMap.get("textureUnitStateMax"))
							.intValue();
					data.add("Texture unit state maximum: " + max);
				}
				if (c3dMap.containsKey("textureWidthMax")) {
					int max = ((Integer) c3dMap.get("textureWidthMax"))
							.intValue();
					data.add("Texture max width: " + max);
				}
				if (c3dMap.containsKey("textureHeightMax")) {
					int max = ((Integer) c3dMap.get("textureHeightMax"))
							.intValue();
					data.add("Texture max height: " + max);
				}
				if (c3dMap.containsKey("native.version")) {
					data.add("Graphic library v.no: "
							+ c3dMap.get("native.version") + "\n");
				}
				return data;
			}

			@Override
			public void postRender() {
				Dimension d = new Dimension();
				this.getSize(d);
				this.getGraphics2D().setColor(Color.white);
				int i = info.size();
				this.getGraphics2D().drawString("FPS: " + getFps(), 3,
						d.height - (info.size() + 1) * 12 + 9);
				for (int j = 0; j < i; j++) {
					this.getGraphics2D().drawString(info.get(j), 3,
							d.height - ((i - j) * 12) + 9);
				}
				this.getGraphics2D().flush(false);
			}
		};
	}

	/**
	 * Reads the basic information about the simulated world from the file
	 * passed as input.
	 * 
	 * @param geomFile
	 *            The {@code .geom} file containing the simulation information.
	 * @throws IOException
	 *             If an I/O error occurs.
	 */
	private void extractPoints(File geomFile) throws IOException {
		BufferedReader br = new BufferedReader(new FileReader(geomFile));
		String line;
		while ((line = br.readLine()) != null) {
			if (line.length() < 2)
				continue;
			if (min != null && max != null && splitAxis != 0 && offset != -1
					&& locals != -1)
				break;
			line = line.substring(2);
			String[] split = line.split(" ");
			if (line.contains("min")) {
				min = new Point3d();
				min.x = Double.parseDouble(split[1]);
				min.y = Double.parseDouble(split[2]);
				min.z = Double.parseDouble(split[3]);
			}
			if (line.contains("max")) {
				max = new Point3d();
				max.x = Double.parseDouble(split[1]);
				max.y = Double.parseDouble(split[2]);
				max.z = Double.parseDouble(split[3]);
			}
			if (line.contains("split")) {
				splitAxis = SPLITX; // default case
				if (line.contains("y"))
					splitAxis = SPLITY;
				else if (line.contains("z"))
					splitAxis = SPLITZ;
			}
			if (line.contains("offset"))
				offset = Double.parseDouble(split[1]);
			if (line.contains("locals"))
				locals = Integer.parseInt(split[1]);
		}
		br.close();
	}

	/** Initial set up of the scene */
	private void createSceneGraph() {
		sceneBranchGroup = new BranchGroup();
		sceneBranchGroup.setCapability(Group.ALLOW_CHILDREN_READ);
		sceneBranchGroup.setCapability(Group.ALLOW_CHILDREN_WRITE);
		sceneBranchGroup.setCapability(Group.ALLOW_CHILDREN_EXTEND);
		sceneBranchGroup.setCapability(Node.ALLOW_PARENT_READ);
		bounds = new BoundingSphere(new Point3d(0, 0, 0), BOUNDSIZE);
	}

	/**
	 * Generated a transparent {@code Shape3D} with dashed edges. It represents
	 * the simulated world.
	 */
	private void drawSimulatedWorld() {
		Appearance app = new Appearance();
		PolygonAttributes polyAttribs = new PolygonAttributes(
				PolygonAttributes.POLYGON_LINE, PolygonAttributes.CULL_NONE, 0);
		LineAttributes la = new LineAttributes(2, LineAttributes.PATTERN_DASH,
				true);
		// TODO Luca: make the dashed lines appear of a different color from white
		app.setPolygonAttributes(polyAttribs);
		app.setLineAttributes(la);
		//
		LineArray lines = new LineArray(24 + locals * 8,
				GeometryArray.COORDINATES);
		// BASE
		lines.setCoordinate(0, new Point3d(min.x, min.y, min.z));
		lines.setCoordinate(1, new Point3d(min.x, min.y, max.z));
		lines.setCoordinate(2, new Point3d(max.x, min.y, max.z));
		lines.setCoordinate(3, new Point3d(min.x, min.y, max.z));
		lines.setCoordinate(4, new Point3d(min.x, min.y, min.z));
		lines.setCoordinate(5, new Point3d(max.x, min.y, min.z));
		lines.setCoordinate(6, new Point3d(max.x, min.y, min.z));
		lines.setCoordinate(7, new Point3d(max.x, min.y, max.z));
		// VERTICAL EDGES
		lines.setCoordinate(8, new Point3d(min.x, min.y, min.z));
		lines.setCoordinate(9, new Point3d(min.x, max.y, min.z));
		lines.setCoordinate(10, new Point3d(max.x, min.y, max.z));
		lines.setCoordinate(11, new Point3d(max.x, max.y, max.z));
		lines.setCoordinate(12, new Point3d(max.x, min.y, min.z));
		lines.setCoordinate(13, new Point3d(max.x, max.y, min.z));
		lines.setCoordinate(14, new Point3d(min.x, min.y, max.z));
		lines.setCoordinate(15, new Point3d(min.x, max.y, max.z));
		// TOP
		lines.setCoordinate(16, new Point3d(min.x, max.y, min.z));
		lines.setCoordinate(17, new Point3d(min.x, max.y, max.z));
		lines.setCoordinate(18, new Point3d(max.x, max.y, max.z));
		lines.setCoordinate(19, new Point3d(min.x, max.y, max.z));
		lines.setCoordinate(20, new Point3d(min.x, max.y, min.z));
		lines.setCoordinate(21, new Point3d(max.x, max.y, min.z));
		lines.setCoordinate(22, new Point3d(max.x, max.y, min.z));
		lines.setCoordinate(23, new Point3d(max.x, max.y, max.z));
		// MIDDLE POINTS
		int count = 24;
		double value;
		switch (splitAxis) {

		case SPLITX:
			for (int i = 1; i < locals; i++) {
				value = min.x + offset * i;
				lines.setCoordinate(count++, new Point3d(value, min.y, min.z));
				lines.setCoordinate(count++, new Point3d(value, min.y, max.z));
				lines.setCoordinate(count++, new Point3d(value, min.y, max.z));
				lines.setCoordinate(count++, new Point3d(value, max.y, max.z));
				lines.setCoordinate(count++, new Point3d(value, max.y, max.z));
				lines.setCoordinate(count++, new Point3d(value, max.y, min.z));
				lines.setCoordinate(count++, new Point3d(value, max.y, min.z));
				lines.setCoordinate(count++, new Point3d(value, min.y, min.z));
			}
			break;
		case SPLITY:
			for (int i = 1; i < locals; i++) {
				value = min.y + offset * i;
				lines.setCoordinate(count++, new Point3d(min.x, value, min.z));
				lines.setCoordinate(count++, new Point3d(min.x, value, max.z));
				lines.setCoordinate(count++, new Point3d(min.x, value, max.z));
				lines.setCoordinate(count++, new Point3d(max.x, value, max.z));
				lines.setCoordinate(count++, new Point3d(max.x, value, max.z));
				lines.setCoordinate(count++, new Point3d(max.x, value, min.z));
				lines.setCoordinate(count++, new Point3d(max.x, value, min.z));
				lines.setCoordinate(count++, new Point3d(min.y, value, min.z));
			}
			break;
		case SPLITZ:
			for (int i = 1; i < locals; i++) {
				value = min.z + offset * i;
				lines.setCoordinate(count++, new Point3d(min.x, min.y, value));
				lines.setCoordinate(count++, new Point3d(min.x, max.y, value));
				lines.setCoordinate(count++, new Point3d(min.x, max.y, value));
				lines.setCoordinate(count++, new Point3d(max.x, max.y, value));
				lines.setCoordinate(count++, new Point3d(max.x, max.y, value));
				lines.setCoordinate(count++, new Point3d(max.x, min.y, value));
				lines.setCoordinate(count++, new Point3d(max.x, min.y, value));
				lines.setCoordinate(count++, new Point3d(min.x, min.y, value));
			}
		}
		// insert in the scene the box representing the simulated space
		sceneBranchGroup.addChild(new Shape3D(lines, app));
	}

	/**
	 * Generates the coordinate axes shape positioned at the origin.
	 */
	private void drawSimulatedWorldCoordinateFrameAtOrigin() {
		Appearance app = new Appearance();
		PolygonAttributes polyAttribs = new PolygonAttributes(
				PolygonAttributes.POLYGON_LINE, PolygonAttributes.CULL_NONE, 0);
		LineAttributes la = new LineAttributes(2, LineAttributes.PATTERN_SOLID,
				true);
		app.setPolygonAttributes(polyAttribs);
		app.setLineAttributes(la);
		//
		LineArray lines = new LineArray(18, GeometryArray.COORDINATES);
		// X
		lines.setCoordinate(0, new Point3d(0, 0, 0));
		lines.setCoordinate(1, new Point3d(1, 0, 0));
		lines.setCoordinate(2, new Point3d(1, 0, 0));
		lines.setCoordinate(3, new Point3d(0.9, 0.1, 0));
		lines.setCoordinate(4, new Point3d(1, 0, 0));
		lines.setCoordinate(5, new Point3d(0.9, -0.1, 0));
		// Y
		lines.setCoordinate(6, new Point3d(0, 0, 0));
		lines.setCoordinate(7, new Point3d(0, 1, 0));
		lines.setCoordinate(8, new Point3d(0, 1, 0));
		lines.setCoordinate(9, new Point3d(0.1, 0.9, 0));
		lines.setCoordinate(10, new Point3d(0, 1, 0));
		lines.setCoordinate(11, new Point3d(-0.1, 0.9, 0));
		// Z
		lines.setCoordinate(12, new Point3d(0, 0, 0));
		lines.setCoordinate(13, new Point3d(0, 0, 1));
		lines.setCoordinate(14, new Point3d(0, 0, 1));
		lines.setCoordinate(15, new Point3d(0, 0.1, 0.9));
		lines.setCoordinate(16, new Point3d(0, 0, 1));
		lines.setCoordinate(17, new Point3d(0, -0.1, 0.9));
		//
		// letters
		Font3D font = new Font3D(new Font("Arial", Font.PLAIN, 1),
				new FontExtrusion());
		Text3D x = new Text3D(font, "X", new Point3f(3.5f, -1.3f, 0));
		Text3D y = new Text3D(font, "Y", new Point3f(-1.3f, 3.5f, 0));
		Text3D z = new Text3D(font, "Z", new Point3f(0, -1.3f, 3.5f));
		OrientedShape3D letterx = new OrientedShape3D(x, app,
				OrientedShape3D.ROTATE_NONE, new Point3f(3.5f, -1.3f, 0));
		OrientedShape3D lettery = new OrientedShape3D(y, app,
				OrientedShape3D.ROTATE_NONE, new Point3f(-1.3f, 3.5f, 0));
		OrientedShape3D letterz = new OrientedShape3D(z, app,
				OrientedShape3D.ROTATE_NONE, new Point3f(0, -1.3f, 3.5f));
		TransformGroup axes = new TransformGroup();
		axes.addChild(new Shape3D(lines, app));
		axes.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		axes.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		TransformGroup lettersT = new TransformGroup();
		lettersT.addChild(letterx);
		lettersT.addChild(lettery);
		lettersT.addChild(letterz);
		lettersT.getTransform(t3d);
		t3d.setScale(0.2);
		lettersT.setTransform(t3d);
		BranchGroup branchNode = new BranchGroup();
		branchNode.addChild(axes);
		branchNode.addChild(lettersT);
		sceneBranchGroup.addChild(branchNode);
	}

	/** Sets the scene lights: one ambient light and six directional lights. */
	private void putLightsOnTheScene() {
		Color3f white = new Color3f(1.0f, 1.0f, 1.0f);
		// Set up the ambient light
		AmbientLight ambientLightNode = new AmbientLight(white);
		ambientLightNode.setInfluencingBounds(bounds);
		sceneBranchGroup.addChild(ambientLightNode);
		// Set up the directional lights
		Vector3f light1Direction = new Vector3f(-1.0f, 0f, 0f);
		Vector3f light2Direction = new Vector3f(0f, -1.0f, 0f);
		Vector3f light3Direction = new Vector3f(0f, 0f, -1.0f);
		Vector3f light4Direction = new Vector3f(1.0f, 0f, 0.0f);
		Vector3f light5Direction = new Vector3f(0.0f, -1.0f, 0.0f);
		Vector3f light6Direction = new Vector3f(0.0f, 0.0f, -1.0f);
		DirectionalLight light1 = new DirectionalLight(white, light1Direction);
		light1.setInfluencingBounds(bounds);
		sceneBranchGroup.addChild(light1);
		DirectionalLight light2 = new DirectionalLight(white, light2Direction);
		light2.setInfluencingBounds(bounds);
		sceneBranchGroup.addChild(light2);
		DirectionalLight light3 = new DirectionalLight(white, light3Direction);
		light3.setInfluencingBounds(bounds);
		sceneBranchGroup.addChild(light3);
		DirectionalLight light4 = new DirectionalLight(white, light4Direction);
		light4.setInfluencingBounds(bounds);
		sceneBranchGroup.addChild(light4);
		DirectionalLight light5 = new DirectionalLight(white, light5Direction);
		light5.setInfluencingBounds(bounds);
		sceneBranchGroup.addChild(light5);
		DirectionalLight light6 = new DirectionalLight(white, light6Direction);
		light6.setInfluencingBounds(bounds);
		sceneBranchGroup.addChild(light6);
	}

	/** Allows to save a screenshot of the simulation. */
	public void getScreenShot() {
		boolean status = getTimer().isPlay();
		try {
			getTimer().setPlay(false);
			int result = fc.showSaveDialog(this);
			if (result != JFileChooser.APPROVE_OPTION) {
				getTimer().setPlay(status);
				return;
			}
			GraphicsContext3D ctx = canvas3D.getGraphicsContext3D();
			java.awt.Dimension scrDim = canvas3D.getSize();
			ImageComponent2D image = new ImageComponent2D(
					ImageComponent.FORMAT_RGB, scrDim.width, scrDim.height);
			Raster ras = new Raster();
			ras.setType(Raster.RASTER_COLOR);
			ras.setCapability(Raster.ALLOW_IMAGE_READ);
			ras.setCapability(Raster.ALLOW_IMAGE_WRITE);
			ras.setSize(scrDim);
			ras.setImage(image);
			ctx.readRaster(ras);
			BufferedImage img = ras.getImage().getImage();
			File file = fc.getSelectedFile();
			if (!file.exists() || !file.getAbsolutePath().endsWith(".jpg")) {
				String fileName = file.getAbsolutePath();
				fileName = fileName.endsWith(".jpg") ? fileName : fileName
						+ ".jpg";
				file = new File(fileName);
				file.createNewFile();
			}
			ImageIO.write(img, "jpg", file); // ignore returned boolean
			HermesLogger.log(HermesLogger.INFO, getClass().getSimpleName()
					+ " - " + "screenshot correctly saved...");
		} catch (IOException e) {
			System.err.println("Failed to save image: " + e);
			HermesLogger.log(HermesLogger.INFO, getClass().getSimpleName()
					+ " - " + "failed to save simulation screenshot...");
		}
		getTimer().setPlay(status);
	}

	/** Sets the scene background. */
	private void setSceneBackgroundColor() {
		Background back = new Background();
		back.setApplicationBounds(bounds);
		back.setColor(0.01f, 0.01f, 0.01f); // sky colour
		sceneBranchGroup.addChild(back);
	}

	/** Allows the user to rotate around the scene, and to zoom in and out. */
	private void orbitControls() {
		orbit = new OrbitBehavior(canvas3D, OrbitBehavior.REVERSE_ALL);
		orbit.setSchedulingBounds(bounds);
		orbit.setTransFactors(3, 3);
		orbit.setRotFactors(1.5, 1.5);
		orbit.setMinRadius(10);
		orbit.setRotXFactor(0);
		orbit.setZoomFactor(5);
		ViewingPlatform vp = simpleUniverse.getViewingPlatform();
		vp.setViewPlatformBehavior(orbit);
	}

	/**
	 * Sets the initial viewpoint of the user, using
	 * {@code Transform3D#lookAt(Point3d, Point3d, Vector3d)}
	 */
	private void initUserPosition(Point3d point) {
		ViewingPlatform vp = simpleUniverse.getViewingPlatform();
		steerTG = vp.getViewPlatformTransform();
		steerTG.getTransform(t3d);
		t3d.lookAt(point, center, new Vector3d(0, 1, 0));
		t3d.invert();
		steerTG.setTransform(t3d);
	}

	/**
	 * Rotates the {@code Viewport} on the Y-axis of the given angle.
	 * 
	 * @param angle
	 *            The new angle to rotate the view.
	 */
	void rotateViewY(double angle) {
		// PREVIOUS VERSION
		// simpleUniverse.getViewingPlatform().getViewPlatformTransform().getTransform(ty);
		// steerTG.getTransform(t3d);
		// ty.rotY(Math.toRadians(angle - previousValue));
		// tempVector.set(0, 0, 0.00005);
		// ty.setTranslation(tempVector);
		// previousValue = angle;
		// ty.mul(t3d);
		// steerTG.setTransform(ty);
		Transform3D rot = new Transform3D();
		Vector3d trans = new Vector3d(), diff = new Vector3d();
		steerTG.getTransform(rot);
		trans.set(0, 0, 0);
		// rot now contains the current rotation but a translation of (0,0,0)
		rot.get(tempVector);
		rot.setTranslation(trans);
		diff.sub(center, tempVector);
		ty.rotY(Math.toRadians(angle - previousValue));
		previousValue = angle;
		steerTG.getTransform(t3d);
		t3d.mul(ty);
		//
		rot.invert();
		rot.transform(diff);
		ty.transform(diff);
		rot.invert();
		rot.transform(diff);
		trans.sub(center, diff);
		t3d.setTranslation(trans);
		steerTG.setTransform(t3d);
	}

	/**
	 * Moves the {@code Viewport} closer to the simulation world or farther,
	 * depending on the passed flag. The view cannot be moved too
	 * 
	 * @param forward
	 *            {@code true} if the camera must move closer to the simulation
	 *            world, {@code false} otherwise.
	 */
	void zoomIO(boolean forward) {
		steerTG.getTransform(t3d);
		t3d.get(tempVector);
		steerTG.getTransform(ty);
		if (forward)
			ty.set(new Vector3d(0, 0, -6));
		else
			ty.set(new Vector3d(0, 0, 6));
		t3d.mul(ty);
		steerTG.setTransform(t3d);
	}

	/**
	 * Creates the {@code Behaviour} for the visualiser. One behaviour is
	 * responsible for calculating the FPS of the visualiser whereas the other
	 * must populate visualiser with the shapes and move them, according to the
	 * {@code #UPDATE_TIME} ms.
	 * 
	 * @param geomFile file .geom generated by the simulation
	 * 
	 */
	private void setUpBehaviours(File geomFile) {
		try {
			timer = (new UpdateBehaviour(geomFile, sceneBranchGroup, getUpdateTime()));
			getTimer().setSchedulingBounds(
					new BoundingSphere(USERPOSN, Double.MAX_VALUE));
			sceneBranchGroup.addChild(timer);
			FpsBehavior fpsBe = new FpsBehavior(this);
			fpsBe.setSchedulingBounds(new BoundingSphere(USERPOSN,
					Double.MAX_VALUE));
			sceneBranchGroup.addChild(fpsBe);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	void addChild(TransShape ts) {
		sceneBranchGroup.addChild(ts.tg);
	}

	/** Builds the panel controls (buttons and sliders) and adds listeners */
	private void createControls() {
		fc = new JFileChooser();
		FileNameExtensionFilter filter = new FileNameExtensionFilter(
				"JPG file", "jpg");
		fc.setFileFilter(filter);
		//
		controls = new JPanel(new FlowLayout());
		play = new JButton();
		playIcon = new ImageIcon(
				Monitor.class.getResource("images/play32_.png"));
		pauseIcon = new ImageIcon(
				Monitor.class.getResource("images/pause32_.png"));
		play.setIcon(playIcon);
		play.setToolTipText("Start/pause the visualization");
		play.setActionCommand("play");
		play.addActionListener(this);
		play.setFocusable(false);
		JButton stop = new JButton();
		stop.setIcon(new ImageIcon(Monitor.class
				.getResource("images/stop32_.png")));
		stop.setToolTipText("Stop the visualization");
		stop.setActionCommand("stop");
		stop.addActionListener(this);
		stop.setFocusable(false);
		JButton snapshot = new JButton();
		snapshot.setIcon(new ImageIcon(Monitor.class
				.getResource("images/shot32.png")));
		snapshot.setToolTipText("Takes a snapshot of the simulation");
		snapshot.setActionCommand("snap");
		snapshot.addActionListener(this);
		snapshot.setFocusable(false);
		JButton reset = new JButton();
		reset.setIcon(new ImageIcon(Monitor.class
				.getResource("images/reset32.png")));
		reset.setToolTipText("Reset the visualization to the initial "
				+ "position");
		reset.setActionCommand("reset");
		reset.addActionListener(this);
		reset.setFocusable(false);
		//
		SpinnerModel model = new SpinnerNumberModel(10, 1, 10000, 10);
		final JSpinner updateTime = new JSpinner(model);
		updateTime.setToolTipText("Change the rendering speed");
		updateTime.setValue(new Integer(getUpdateTime()));
		updateTime.addChangeListener(new ChangeListener() {
			@Override
			public void stateChanged(ChangeEvent arg0) {
				getTimer().setTimeOut(
						((Integer) updateTime.getValue()).intValue());
			}
		});
		//
		slider = new JSlider(-180, 180, 0);
		slider.setPaintTicks(true);
		slider.setPreferredSize(new Dimension(400, 50));
		slider.setPaintLabels(true);
		slider.setLabelTable(getTable());
		slider.setMinorTickSpacing(10);
		slider.setMajorTickSpacing(10);
		slider.addMouseMotionListener(new MouseMotionAdapter() {

			@SuppressWarnings("synthetic-access")
			@Override
			public void mouseDragged(MouseEvent e) {
				rotateViewY(slider.getValue());
			}
		});
		//
		slider2 = new JSlider(SwingConstants.VERTICAL, -1, 1, 0);
		slider2.setPreferredSize(new Dimension(80, 80));
		slider2.setPaintLabels(true);
		slider2.setLabelTable(getTableZoom());
		slider2.setMinorTickSpacing(1);
		slider2.setMajorTickSpacing(1);
		slider2.addMouseListener(new MouseAdapter() {
			@SuppressWarnings("synthetic-access")
			@Override
			public void mouseClicked(MouseEvent e) {
				if (slider2.getValue() == 1)
					zoomIO(true);
				else if (slider2.getValue() == -1)
					zoomIO(false);
				slider2.setValue(0);
			}

			@Override
			public void mousePressed(MouseEvent e) {
				// DOES NOTHING
			}
		});
		//
		slider2.addMouseMotionListener(new MouseMotionAdapter() {
			double previusValue;

			@SuppressWarnings("synthetic-access")
			@Override
			public void mouseDragged(MouseEvent e) {
				// if (e.getComponent() == null)
				// previusValue = 0;
				if (slider2.getValue() > previusValue)
					zoomIO(true);
				else if (slider2.getValue() < previusValue)
					zoomIO(false);
				slider2.setValue(0);
				previusValue = 0;
			}
		});
		//
		JPanel buttons = new JPanel(new GridLayout(2, 2));
		buttons.add(play);
		buttons.add(stop);
		buttons.add(snapshot);
		buttons.add(reset);
		//
		JPanel rotation = new JPanel();
		rotation.setLayout(new BoxLayout(rotation, BoxLayout.Y_AXIS));
		controls.add(buttons);
		//
		JLabel label = new JLabel("Horizontal rotation");
		label.setAlignmentX(Component.CENTER_ALIGNMENT);
		rotation.add(label, BorderLayout.NORTH);
		rotation.add(slider, BorderLayout.SOUTH);
		controls.add(rotation);
		//
		controls.add(slider2);
		//
		JPanel update = new JPanel();
		JLabel label1 = new JLabel("Render time (ms)");
		label1.setAlignmentX(Component.CENTER_ALIGNMENT);
		updateTime.setAlignmentY(Component.CENTER_ALIGNMENT);
		update.setLayout(new BoxLayout(update, BoxLayout.Y_AXIS));
		update.add(label1);
		update.add(updateTime);
		controls.add(update);
		//
		add(controls, BorderLayout.SOUTH);
	}

	/**
	 * Values for rotation sliders labels
	 * 
	 * @return An {@code Hashtable} containing the values for the sliders
	 */
	private Hashtable<Integer, JLabel> getTable() {
		Hashtable<Integer, JLabel> table = new Hashtable<Integer, JLabel>();
		table.put(new Integer(-180), new JLabel("-180"));
		table.put(new Integer(-90), new JLabel("-90"));
		table.put(new Integer(0), new JLabel("0"));
		table.put(new Integer(90), new JLabel("90"));
		table.put(new Integer(180), new JLabel("180"));
		return table;
	}

	/**
	 * Values for rotation sliders labels
	 * 
	 * @return An {@code Hashtable} containing the values for the sliders
	 */
	private Hashtable<Integer, JLabel> getTableZoom() {
		Hashtable<Integer, JLabel> table = new Hashtable<Integer, JLabel>();
		table.put(new Integer(-1), new JLabel("Zoom out"));
		table.put(new Integer(1), new JLabel("Zoom in"));
		return table;
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		String action = e.getActionCommand();
		if (action.equals("reset")) {
		    initUserPosition(USERPOSN);
            slider.setValue(0);
            orbit.goHome();
            previousValue = 0;
		} else if (action.equals("snap")) {
		    getScreenShot();
		} else if (action.equals("play")) {
		    if (getTimer().isPlay()) {
                getTimer().setPlay(false);
                play.setIcon(playIcon);
            } else {
                getTimer().setPlay(true);
                play.setIcon(pauseIcon);
            }
		} else if (action.equals("stop")) {
		    if (getTimer().isPlay()) {
                timer.setPlay(false);
                play.setIcon(playIcon);
            }
            timer.resetPositions();
		}
	}

	/**
	 * Setter.
	 * 
	 * @param fps
	 *            the fps to set
	 */
	public void setFPS(String fps) {
		this.fps = fps;
	}

	/** @return the fps of the panel */
	public String getFps() {
		return fps;
	}

	/** @return the rendering update time */
	public static int getUpdateTime() {
		return UPDATE_TIME;
	}

	/** @return the timer */
	public UpdateBehaviour getTimer() {
		return timer;
	}

	void dispose() {
		timer.destroy();
	}
}